package com.mscsoft.ui;

import java.io.IOException;
import java.net.URL;
import java.util.*;
import javax.sound.sampled.*;

import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.effect.Bloom;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Duration;
import javafx.util.Pair;
import javafx.application.Platform;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.mscsoft.App;
import com.mscsoft.judge.Judger;
import com.mscsoft.musicscore.*;
import com.mscsoft.musicscore.note.*;
import com.mscsoft.musicscore.note.Note.NoteType;
import com.mscsoft.tools.ConTools;

/** 打歌界面 */
public class Play extends Application {
    // 日志
    private static final Logger logger = LogManager.getLogger(Play.class.getSimpleName());
    // 显示相关
    private BorderPane bd = new BorderPane();// 显示窗格
    private final int resolutionX = App.getResolutionX();// 横向分辨率
    private final int resolutionY = App.getResolutionY();// 纵向分辨率
    private static final int jgLineY = App.getResolutionY() * 8 / 9;// 判定线高度
    private final int fallingTime = 200
            * (10 - Math.min(Math.max(Integer.parseInt((String) App.properties.get("speed")), 0), 9));// 音符下落时间(ms)
    private static final double sideWidthR = 0.1;// 两侧空白比
    private double intervalWidthR = 0.04;// 音符间距比
    private int column;// 铺面轨道数 4/6/8/10k
    private int noteWidth; // 音符宽度
    private int noteHeight;// 音符高度

    private HBox sCombo = new HBox();// 一位数combo的Hbox
    private HBox dCombo = new HBox();// 两位数combo的Hbox
    private HBox tCombo = new HBox();// 三位数combo的Hbox
    private HBox qCombo = new HBox();// 四位数combo的Hbox

    // 音符动画相关
    private MusicScore ms;// 待显示的谱面
    private final List<Timeline> timelines = new ArrayList<>();// 音符动画
    private final List<Double> timelineDelays = new ArrayList<>();// 音符延迟
    private List<TreeMap<Double, Pair<Note, ImageView>>> tracks = new ArrayList<>();// 轨道列表(包含音符与其图标的图)
    // 音频相关
    private URL audio;// 待播放的音频
    private SourceDataLine line = null;// 音频播放线路
    private float audioFrameRate = 44100f;// 音频帧帧率(自动获取,默认为44.1k)
    // 判定相关
    private Judger judger;
    // 资源路径
    Image tapImg = new Image(Play.class.getResourceAsStream("/static/img/Tap.png"));// tap图标
    Image holdImg = new Image(Play.class.getResourceAsStream("/static/img/Hold.png"));// hold图标
    Image gameSceneBG = new Image(Play.class.getResourceAsStream("/static/img/GameSceneBG_1.png"), resolutionX,
            resolutionY, false, false);// 背景
    Image jgLine = new Image(Play.class.getResourceAsStream("/static/img/JudgeLine.png"));// 判定线
    Image jgLineFC = new Image(Play.class.getResourceAsStream("/static/img/JudgeLineFC.png"));// 判定线
    Image jgLineAP = new Image(Play.class.getResourceAsStream("/static/img/JudgeLineAP.png"));// 判定线
    Image[] num = new Image[10];// 数字图标
    Image[] comboNum = new Image[10];// combo数字图标
    ImageView[] scoreView = new ImageView[7];
    ImageView[] comboViewS = new ImageView[1];// 一位数combo的图标
    ImageView[] comboViewD = new ImageView[2];// 两位数combo的图标
    ImageView[] comboViewT = new ImageView[3];// 三位数combo的图标
    ImageView[] comboViewQ = new ImageView[4];// 四位数combo的图标

    private Stage stage;

    @Override
    public void start(Stage primaryStage) throws Exception {
        stage = primaryStage;
        Scene scene = new Scene(bd, resolutionX, resolutionY);
        // 加载静态资源
        loadStaticResources();
        // 加载音符
        addNotes();
        // 播放音频、动画
        playMedia();
        // 创建自动清理超时未判定音符的进程
        creAutomaticCleaningThreads();
        // 创建自动完成Hold音符尾判的进程
        creAutomaticJudHoldEThreads();
        // 舞台显示
        primaryStage.setScene(scene);
        primaryStage.show();
        // 事件监听
        keyEventListeners();
        primaryStage.setOnCloseRequest(e -> System.exit(0));
    }

    /** 设定游玩谱面 */
    public void setMS(MusicScore ms) {
        this.ms = ms;
        // 从谱面获取音轨数量
        column = ms.meta.column;
        intervalWidthR = 0.04 - (column - 4) * 0.005;
        double noteWidthR = (1 - sideWidthR * 2 - intervalWidthR * (column - 1)) / column;// 音符宽度比
        noteWidth = (int) Math.round(resolutionX * noteWidthR);
        noteHeight = (int) Math.round(0.17 * 100 / 989 * resolutionX);// 该高度按4k时音符宽度计算(参考图标比例)
        // 从谱面获取音频路径
        audio = Play.class.getResource("/music/" + ms.sound.soundPath);
        // 初始化轨道列表
        for (int i = 0; i < column; i++) {
            tracks.add(new TreeMap<>());
        }
        // 初始化判定器
        judger = new Judger(ms.bpm, ms.notes.size());
        earlyTime = -judger.missL;
        lateTime = judger.badL;
        // 初始化键盘映射
        switch (column) {
            case 2:
                keyMapping = new int[] { -1, -1, -1, 0, -1, -1, 1, -1, -1, -1 };
                break;
            case 4:
                keyMapping = new int[] { -1, -1, 0, 1, -1, -1, 2, 3, -1, -1 };
                break;
            case 6:
                keyMapping = new int[] { -1, 0, 1, 2, -1, -1, 3, 4, 5, -1 };
                break;
            case 8:
                keyMapping = new int[] { 0, 1, 2, 3, -1, -1, 4, 5, 6, 7 };
                break;
            case 10:
                keyMapping = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
                break;
            default:
        }
    }

    // 1.资源加载播放相关
    ImageView jgLineView = new ImageView(jgLineAP);

    /** 加载静态资源 */
    private void loadStaticResources() {
        // 打歌背景
        ImageView gameSceneBGView = new ImageView(gameSceneBG);
        bd.setCenter(gameSceneBGView);
        // 判定线
        jgLineView.setOpacity(0.8);
        bd.getChildren().add(jgLineView);
        jgLineView.setX(0);
        jgLineView.setY(jgLineY);
        // 分数显示
        HBox scoreHBox = new HBox();
        for (int i = 0; i < num.length; i++) {
            num[i] = new Image(Play.class.getResourceAsStream("/static/img/num/" + i + ".png"));
        }
        for (int i = 0; i < scoreView.length; i++) {
            scoreView[i] = new ImageView(num[0]);
            scoreHBox.getChildren().add(scoreView[i]);
        }
        scoreHBox.setPrefSize(36, 72);
        scoreHBox.setLayoutX(resolutionX - scoreHBox.getPrefWidth() * 7);// 七位数向右对齐
        bd.getChildren().add(scoreHBox);
        // combo显示
        for (int i = 0; i < comboNum.length; i++) {
            comboNum[i] = new Image(Play.class.getResourceAsStream("/static/img/comboNum/" + i + ".png"));
        }

        comboViewS[0] = new ImageView(comboNum[0]);// 一位combo的图标初始化

        for (int i = 0; i < comboViewD.length; i++) {// 两位combo的图标初始化
            comboViewD[i] = new ImageView(comboNum[0]);
        }

        for (int i = 0; i < comboViewT.length; i++) {// 三位combo的图标初始化
            comboViewT[i] = new ImageView(comboNum[0]);
        }

        for (int i = 0; i < comboViewQ.length; i++) {// 四位combo的图标初始化
            comboViewQ[i] = new ImageView(comboNum[0]);
        }
        // 一位数combo的Hbox的设置
        sCombo.getChildren().add(comboViewS[0]);
        sCombo.setPrefSize(58, 81);
        sCombo.setLayoutX((resolutionX - sCombo.getPrefWidth()) / 2.0);
        sCombo.setLayoutY(sCombo.getPrefHeight());
        // 两位数combo的Hbox的设置
        dCombo.getChildren().addAll(comboViewD[0], comboViewD[1]);
        dCombo.setPrefSize(116, 81);
        dCombo.setLayoutX((resolutionX - dCombo.getPrefWidth()) / 2.0);
        dCombo.setLayoutY(dCombo.getPrefHeight());
        // 三位数combo的Hbox的设置
        tCombo.getChildren().addAll(comboViewT[0], comboViewT[1], comboViewT[2]);
        tCombo.setPrefSize(174, 81);
        tCombo.setLayoutX((resolutionX - tCombo.getPrefWidth()) / 2.0);
        tCombo.setLayoutY(tCombo.getPrefHeight());
        // 四位数combo的Hbox的设置
        qCombo.getChildren().addAll(comboViewQ[0], comboViewQ[1], comboViewQ[2], comboViewQ[3]);
        qCombo.setPrefSize(232, 81);
        qCombo.setLayoutX((resolutionX - qCombo.getPrefWidth()) / 2.0);
        qCombo.setLayoutY(qCombo.getPrefHeight());
        // combo的Hbox的设置
        bd.getChildren().addAll(sCombo, dCombo, tCombo, qCombo);
        sCombo.setOpacity(0);
        dCombo.setOpacity(0);
        tCombo.setOpacity(0);
        qCombo.setOpacity(0);
    }

    private int userOffset = 0;// 用户设定的延迟校准

    /** 预加载音符至画面 */
    private void addNotes() {
        for (Note note : ms.notes) {
            if (note.type == NoteType.TAP) {
                // 设置image:tap的参数
                ImageView tap = new ImageView(tapImg);
                tap.setFitWidth(noteWidth);
                tap.setFitHeight(noteHeight);
                tap.setY((jgLineY - resolutionY));
                tap.setX(resolutionX / 2.0 + (note.trackID - column / 2.0) * noteWidth
                        + (resolutionX * intervalWidthR) * (note.trackID - column / 2.0 + 0.5));
                bd.getChildren().add(tap);

                Timeline timeline = new Timeline();
                timelines.add(timeline);

                double noteDelay = (double) note.tick / 1000;// 音符按照谱面出现的延迟
                double preludeDelay = ms.sound.offset;// 音乐前奏的延迟
                double fallingDelay = fallingTime;// 下落的延迟
                double aDelay = noteDelay + preludeDelay - fallingDelay + userOffset;// 动画延迟
                assert (aDelay >= 0);

                timelineDelays.add(Duration.millis(aDelay).toMillis());// 用于音符显示
                double jDelay = noteDelay + preludeDelay + userOffset;// 判定延迟
                tracks.get(note.trackID).put(Duration.millis(jDelay).toMillis(),
                        new Pair<>(note, tap));// 用于判定及音符消失

                // 将x的位置在fallingTime内移动到判定线处
                final KeyValue kv = new KeyValue(tap.yProperty(), resolutionY);
                final KeyFrame kf = new KeyFrame(Duration.millis(fallingTime * (2.0 - (double) jgLineY / resolutionY)),
                        onFinished -> {
                            timeline.stop();
                            tap.opacityProperty().set(0);
                        }, kv);
                // 将关键帧加到时间轴中
                timeline.getKeyFrames().add(kf);
            } else if (note.type == NoteType.HOLD) {
                // 设置image:hold的参数
                ImageView hold = new ImageView(holdImg);
                hold.setFitWidth(noteWidth);
                hold.setFitHeight(resolutionY / (double) fallingTime * note.duration / 1000);
                hold.setY((jgLineY - resolutionY) - hold.getFitHeight());
                hold.setX(resolutionX / 2.0 + (note.trackID - column / 2.0) * noteWidth
                        + (resolutionX * intervalWidthR) * (note.trackID - column / 2.0 + 0.5));
                bd.getChildren().add(hold);

                Timeline timeline = new Timeline();
                timelines.add(timeline);

                double noteDelay = (double) note.tick / 1000;// 音符按照谱面出现的延迟
                double preludeDelay = ms.sound.offset;// 音乐前奏的延迟
                double fallingDelay = fallingTime;// 下落的延迟
                double aDelay = noteDelay + preludeDelay - fallingDelay + userOffset;// 动画延迟
                assert (aDelay >= 0);

                timelineDelays.add(Duration.millis(aDelay).toMillis());// 用于音符显示
                double jDelay = noteDelay + preludeDelay + userOffset;// 判定延迟
                tracks.get(note.trackID).put(Duration.millis(jDelay).toMillis(),
                        new Pair<>(note, hold));// 用于判定及音符消失

                // 将x的位置在fallingTime内移动到判定线处
                final KeyValue kv = new KeyValue(hold.yProperty(), resolutionY);
                final KeyFrame kf = new KeyFrame(
                        Duration.millis(fallingTime * (2.0 - (double) jgLineY / resolutionY) + note.duration / 1000.0),
                        onFinished -> {
                            timeline.stop();
                            hold.opacityProperty().set(0);
                        }, kv);
                // 将关键帧加到时间轴中
                timeline.getKeyFrames().add(kf);
            }
        }
    }

    /** 播放音频、动画 */
    private void playMedia() throws UnsupportedAudioFileException, IOException {
        // 音频播放
        creAudioPlayer();
        // 动画跟随音频同步播放
        creNotePlayer();
    }

    private void creAudioPlayer() throws UnsupportedAudioFileException, IOException {
        AudioInputStream in = AudioSystem.getAudioInputStream(audio);
        Thread audioPlayThread = new Thread("AudioPlayer") {
            @Override
            public void run() {
                try {
                    logger.info("Successfully read audio: {}", in.getFormat());
                    audioPlay(in.getFormat(), AudioSystem.getAudioInputStream(in.getFormat(), in));
                    Thread.sleep(100);// 音频播放完毕 显示结算结果
                    logger.info("Result: Rating: {}, Score: {}, MaxCombo:{}, Counts: {}", judger.getRating(),
                            judger.getScore(), judger.getMaxCombo(), judger.getCounts());
                    end();// 游戏结束后需要处理的事务
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }

        };
        audioPlayThread.start();
    }

    boolean offline = App.getOffline();

    private void end() {
        if (!offline)
            ConTools.updateScore(ms.meta.id, judger.getScore(), judger.getMaxCombo(), isAP, isFC);
        Platform.runLater(() -> {
            try {
                GameEndScene.set(judger.getRating(), judger.getScore(),
                        judger.getMaxCombo(), judger.getCounts(), ms);
                GameEndScene gameEndScene = new GameEndScene();
                gameEndScene.start(GameEndScene.stage);
                stage.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }

    private void creNotePlayer() {
        Thread notePlayThread = new Thread("NotePlayer") {
            @Override
            public void run() {
                try {
                    int size = timelines.size();
                    logger.info("Successfully read note list: size={}", size);
                    int offset = 0;
                    while (true) {
                        double audioT = (line == null) ? -10 : getAudioCurrentMillis();// 当前音频时间(ms)
                        // 将当前音频时间(ms)前的音符动画播放
                        for (int i = offset; i < size; i++) {
                            Timeline timeline = timelines.get(i);
                            if (timelineDelays.get(i) <= audioT + 1000.0 / 120) {// 至多提前0.5帧播放(60fps)
                                timeline.play();
                            } else {
                                offset = i;
                                break;
                            }
                        }
                        Thread.sleep(8);
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        };
        notePlayThread.start();
    }

    /** 音频播放子方法 */
    private void audioPlay(AudioFormat targetFormat, AudioInputStream din) {
        try {
            int audioFrameSize = targetFormat.getFrameSize();
            audioFrameRate = targetFormat.getFrameRate();
            byte[] data = new byte[audioFrameSize * (int) audioFrameRate];
            line = (SourceDataLine) AudioSystem.getLine(new DataLine.Info(SourceDataLine.class, targetFormat));
            line.open(targetFormat);
            line.start();
            // Start
            for (int bytesR = din.read(data, 0, data.length); bytesR != -1; bytesR = din.read(data, 0, data.length))
                line.write(data, 0, bytesR);
            // Stop
            line.drain();
            line.stop();
            line.close();
            din.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /** 获取当前音频时间轴时间 */
    private double getAudioCurrentMillis() {
        return line.getFramePosition() * 1000.0 / audioFrameRate;
    }

    // 2.判定相关
    double earlyTime;// 最早判定时间
    double lateTime;// 最晚判定时间

    /** 以当前音频时间轴时间判定一次trackID轨道的音符 */
    private void judge(int trackID) {
        if (trackID < 0)
            return;
        TreeMap<Double, Pair<Note, ImageView>> track = tracks.get(trackID);
        if (track.isEmpty())// 轨道无音符: 无视
            return;
        double actual = getAudioCurrentMillis();
        double expected = track.firstKey();
        double offset = actual - expected;
        logger.trace("judge(T{}): expected:{}ms actual:{}ms offset:{}ms", trackID, expected, actual, offset);
        if (offset > lateTime) {// 过晚的输入: 当自动清理线程未及时判定并清理
            Level level = (offset - lateTime > 1000.0 / 60) ? Level.WARN : Level.DEBUG;// 超过16.67ms时会显著影响游玩体验
            logger.log(level, "ACT Cleaning is not timely! Delay={}ms", offset - lateTime);
            judging(trackID, track, expected, offset);// 判定并清理
            judge(trackID);// 尝试为这一次输入调用一次正确的判定
        } else if (offset < earlyTime) {// 过早的输入: 无视
        } else {// 合适的输入: 进行判定
            Pair<Note, ImageView> ni = track.get(expected);
            Note note = ni.getKey();
            if (note.type == NoteType.TAP) {
                judging(trackID, track, expected, offset);
            } else if (note.type == NoteType.HOLD) {
                judgingHold(trackID, track, expected, offset);
            }
        }
    }

    boolean isAP = true;
    boolean isFC = true;

    /** AP/FC指示器 */
    private void indicatorAPorFC() {
        if (isFC) {
            int[] counts = judger.getCounts();
            if (counts[3] > 0 || counts[4] > 0) {// 存在Bad或Miss
                jgLineView.setImage(jgLine);
                isAP = false;
                isFC = false;
            } else if (isAP && counts[2] > 0) {// 存在Good
                jgLineView.setImage(jgLineFC);
                isAP = false;
            }
        }
    }

    /**
     * 进一步判定并计分
     * 线程同步: 防止用户判定函数(judge)和自动清理线程(AutomaticCleaningThreads)同时进入
     */
    private synchronized void judging(int trackID, TreeMap<Double, Pair<Note, ImageView>> track, double expected,
            double offset) {
        if (track.containsKey(expected)) {// 防止同个音符被第二次调用时出错
            Pair<Note, ImageView> ni = track.get(expected);
            Note note = ni.getKey();
            ImageView imageView = ni.getValue();
            if (note.type == NoteType.TAP)
                imageView.setOpacity(0);// 音符消失
            else if (note.type == NoteType.HOLD)
                imageView.setOpacity(0.5);// 长条Miss为半透明
            track.remove(expected);
            double percentage = judger.scoring(offset);// 判定百分比
            String appraise = judger.appraise[judger.getAppraise(percentage)];// 判定评价
            int combo = judger.getCurrentCombo();
            int maxCombo = judger.getMaxCombo();
            int score = judger.getScore();
            // 记录日志
            logger.debug("judge(T{}): {} Combo:{} MaxCombo:{} Score:{}", trackID, appraise, combo, maxCombo, score);
            // 图形化显示
            changeScore(score);
            changeCombo(combo);
            indicatorAPorFC();
        }
    }

    /** 由用户按下按键触发的长条音符判定 */
    private synchronized void judgingHold(int trackID, TreeMap<Double, Pair<Note, ImageView>> track, double expected,
            double offset) {
        Pair<Note, ImageView> ni = track.get(expected);
        Note note = ni.getKey();
        ImageView imageView = ni.getValue();
        track.remove(expected);
        imageView.setEffect(new Bloom(0.1));
        tempOffset[trackID] = offset;
        tempExpected[trackID] = expected + note.duration / 1000.0;
        tempImageView[trackID] = imageView;
        logger.trace("judgingHold(T{}): first key offset:{}", trackID, offset);
    }

    // 在judgingHold与holdReleased间传递的变量
    double[] tempOffset = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 };
    double[] tempExpected = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 };
    ImageView[] tempImageView = new ImageView[10];

    /** 由用户释放按键触发的长条音符判定 */
    private void holdReleased(int trackID) {
        if (trackID < 0 || tempOffset[trackID] < 0)// 说明该轨道目前没有需要持续判定的长条音符
            return;
        // 根据释放时间判定长条是否 Miss
        double actual = getAudioCurrentMillis();// 实际的释放时间
        double expected = tempExpected[trackID];// 预期的释放时间
        double offset = actual - expected;
        // 如果提前了badL以上时间释放 则该Hold音符判miss 否则维持原判定
        double tailOffset = (offset < -judger.badL) ? judger.missL : tempOffset[trackID];
        tempImageView[trackID].setOpacity((offset < -judger.badL) ? 0.5 : 0);// Miss半透明 非Miss音符消失
        double percentage = judger.scoring(tailOffset);// 判定百分比
        String appraise = judger.appraise[judger.getAppraise(percentage)];// 判定评价
        int combo = judger.getCurrentCombo();
        int maxCombo = judger.getMaxCombo();
        int score = judger.getScore();
        // 记录日志
        logger.debug("jHold(T{}): {} Combo:{} MaxCombo:{} Score:{}", trackID, appraise, combo, maxCombo, score);
        // 图形化显示
        changeScore(score);
        changeCombo(combo);
        indicatorAPorFC();
        // 恢复原状
        tempOffset[trackID] = -1;
        tempExpected[trackID] = -1;
    }

    /** 为每个轨道创建自动判定并清理超时音符的线程 */
    private void creAutomaticCleaningThreads() {
        Thread[] threads = new Thread[column];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = creACT(i);
            threads[i].start();
        }
    }

    /** 创建自动判定并清理超时音符的线程 */
    private Thread creACT(int i) {
        return new Thread("AutomaticCleaningThread-" + i) {
            @Override
            public void run() {
                try {
                    int trackID = Integer.parseInt(getName().substring("AutomaticCleaningThread-".length()));
                    TreeMap<Double, Pair<Note, ImageView>> track = tracks.get(trackID);
                    while (line == null)// 等待音频加载
                        Thread.sleep(16);
                    while (!track.isEmpty()) {// 轨道无剩余音符: 结束进程
                        double actual = getAudioCurrentMillis();
                        double expected = track.firstKey();
                        double offset = actual - expected;
                        if (offset > lateTime) {// 过晚的输入: Miss
                            judging(trackID, track, expected, offset);
                            continue;
                        }
                        Thread.sleep(8);
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        };
    }

    /** 为每个轨道创建自动完成Hold音符尾判的线程 */
    private void creAutomaticJudHoldEThreads() {
        Thread[] threads = new Thread[column];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = creAJT(i);
            threads[i].start();
        }
    }

    /** 创建自动完成Hold音符尾判的线程 */
    private Thread creAJT(int i) {
        return new Thread("AutomaticJudHoldEThread-" + i) {
            @Override
            public void run() {
                try {
                    int trackID = Integer.parseInt(getName().substring("AutomaticJudHoldEThread-".length()));
                    while (line == null)// 等待音频加载
                        Thread.sleep(16);
                    while (true) {
                        if (tempOffset[trackID] > 0) {
                            while (getAudioCurrentMillis() < tempExpected[trackID])
                                Thread.sleep(8);
                            holdReleased(trackID);
                        }
                        Thread.sleep(8);
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        };
    }

    // 3.窗体数据更新相关
    /** 改变当前分数的显示数据 */
    private void changeScore(int score) {
        for (int i = 0; i < 7; i++) {
            scoreView[6 - i].setImage(num[score % 10]);
            score /= 10;
        }
    }

    /** 改变当前Combo的显示数据 */
    private void changeCombo(int combo) {
        String combos = String.valueOf(combo);
        if (combo == 0) {
            sCombo.setOpacity(0);
            dCombo.setOpacity(0);
            tCombo.setOpacity(0);
            qCombo.setOpacity(0);
        } else if (combos.length() == 1) {
            comboViewS[0].setImage(comboNum[combo]);
            sCombo.setOpacity(1);
            dCombo.setOpacity(0);
            tCombo.setOpacity(0);
            qCombo.setOpacity(0);
        } else if (combos.length() == 2) {
            comboViewD[0].setImage(comboNum[combo / 10]);
            comboViewD[1].setImage(comboNum[combo % 10]);
            sCombo.setOpacity(0);
            dCombo.setOpacity(1);
            tCombo.setOpacity(0);
            qCombo.setOpacity(0);
        } else if (combos.length() == 3) {
            comboViewT[0].setImage(comboNum[combo / 100]);
            comboViewT[1].setImage(comboNum[(combo / 10) % 10]);
            comboViewT[2].setImage(comboNum[combo % 10]);
            sCombo.setOpacity(0);
            dCombo.setOpacity(0);
            tCombo.setOpacity(1);
            qCombo.setOpacity(0);
        } else if (combos.length() == 4) {
            comboViewQ[0].setImage(comboNum[combo / 1000]);
            comboViewQ[1].setImage(comboNum[(combo / 100) % 10]);
            comboViewQ[2].setImage(comboNum[(combo / 10) % 10]);
            comboViewQ[3].setImage(comboNum[combo % 10]);
            sCombo.setOpacity(0);
            dCombo.setOpacity(0);
            tCombo.setOpacity(0);
            qCombo.setOpacity(1);
        }
    }

    // 4.事件监听相关
    boolean[] isReleased = { true, true, true, true, true, true, true, true, true, true };
    int[] keyMapping;

    /** 按键事件监听 */
    private void keyEventListeners() {
        keyPressed();
        keyReleased();
        bd.requestFocus();
    }

    private void keyReleased() {
        bd.setOnKeyReleased(e -> {
            switch (e.getCode()) {
                case A:
                    holdReleased(keyMapping[0]);
                    isReleased[0] = true;
                    break;
                case S:
                    holdReleased(keyMapping[1]);
                    isReleased[1] = true;
                    break;
                case D:
                    holdReleased(keyMapping[2]);
                    isReleased[2] = true;
                    break;
                case F:
                    holdReleased(keyMapping[3]);
                    isReleased[3] = true;
                    break;
                case V:
                    holdReleased(keyMapping[4]);
                    isReleased[4] = true;
                    break;
                case N:
                    holdReleased(keyMapping[5]);
                    isReleased[5] = true;
                    break;
                case J:
                    holdReleased(keyMapping[6]);
                    isReleased[6] = true;
                    break;
                case K:
                    holdReleased(keyMapping[7]);
                    isReleased[7] = true;
                    break;
                case L:
                    holdReleased(keyMapping[8]);
                    isReleased[8] = true;
                    break;
                case SEMICOLON:
                    holdReleased(keyMapping[9]);
                    isReleased[9] = true;
                    break;
                default:
            }
        });
    }

    private void keyPressed() {
        bd.setOnKeyPressed(e -> {
            switch (e.getCode()) {
                case A:
                    judgeIfReleased(0);
                    break;
                case S:
                    judgeIfReleased(1);
                    break;
                case D:
                    judgeIfReleased(2);
                    break;
                case F:
                    judgeIfReleased(3);
                    break;
                case V:
                    judgeIfReleased(4);
                    break;
                case N:
                    judgeIfReleased(5);
                    break;
                case J:
                    judgeIfReleased(6);
                    break;
                case K:
                    judgeIfReleased(7);
                    break;
                case L:
                    judgeIfReleased(8);
                    break;
                case SEMICOLON:
                    judgeIfReleased(9);
                    break;
                default:
            }
        });
    }

    private void judgeIfReleased(int i) {
        if (isReleased[i])
            judge(keyMapping[i]);
        isReleased[i] = false;
    }
}
